﻿// <copyright file="TestRunner.cs" company="T4 Toolbox Team">
//  Copyright © T4 Toolbox Team. All Rights Reserved.
// </copyright>

namespace T4Toolbox
{
    using System;
    using System.CodeDom.Compiler;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.Reflection;
    using System.Text;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// Executes T4 unit tests.
    /// </summary>
    public static class TestRunner
    {
        /// <summary>
        /// Stores list of errors and warnings until the end of test execution.
        /// </summary>
        private static CompilerErrorCollection errors;
        
        /// <summary>
        /// Stores test execution log until the end of test execution.
        /// </summary>
        private static StringBuilder log;

        /// <summary>
        /// Runs all tests defined in the currently running T4 template.
        /// </summary>
        public static void RunTests()
        {
            TestRunner.errors = new CompilerErrorCollection();
            TestRunner.log = new StringBuilder();
            try
            {
                RunTests(TransformationContext.Transformation.GetType());
                Assembly testAssembly = TransformationContext.Transformation.GetType().Assembly;
                foreach (System.Type type in testAssembly.GetExportedTypes())
                {
                    if (type.GetCustomAttributes(typeof(TestClassAttribute), false).Length > 0)
                    {
                        RunTests(type);
                    }
                }
            }
            finally
            {
                // Clear any errors reported by a <see cref="Generator"/> class under test
                TransformationContext.ClearErrors();

                // Report test execution errors
                TransformationContext.ReportErrors(TestRunner.errors);
                TransformationContext.Host.SetFileExtension(".log");
                TransformationContext.Transformation.Write(log.ToString());
            }
        }

        /// <summary>
        /// Runs all tests defined in the specified test class.
        /// </summary>
        /// <param name="testClass">
        /// <see cref="System.Type"/> of the class that contains methods marked with 
        /// <see cref="TestMethodAttribute"/>.
        /// </param>
        private static void RunTests(System.Type testClass)
        {
            // Collect information about test methods
            List<MethodInfo> testInitializeMethods = new List<MethodInfo>();
            List<MethodInfo> testCleanupMethods = new List<MethodInfo>();
            List<MethodInfo> testMethods = new List<MethodInfo>();
            int maxTestMethodNameLength = 0;
            foreach (MethodInfo method in testClass.GetMethods())
            {
                if (method.GetCustomAttributes(typeof(TestMethodAttribute), false).Length > 0)
                {
                    testMethods.Add(method);
                    maxTestMethodNameLength = System.Math.Max(maxTestMethodNameLength, method.Name.Length);
                }
                else if (method.GetCustomAttributes(typeof(TestInitializeAttribute), false).Length > 0)
                {
                    testInitializeMethods.Add(method);
                }
                else if (method.GetCustomAttributes(typeof(TestCleanupAttribute), false).Length > 0)
                {
                    testCleanupMethods.Add(method);
                }
            }

            // Run tests
            foreach (MethodInfo testMethod in testMethods)
            {
                TestRunner.Log("{0} - ", testMethod.Name.PadRight(maxTestMethodNameLength));
                bool passed = true;
                object testObject = Activator.CreateInstance(testClass);
                try
                {
                    foreach (MethodInfo testInitializeMethod in testInitializeMethods)
                    {
                        passed &= InvokeTestMethod(testInitializeMethod, testObject);
                    }

                    passed &= InvokeTestMethod(testMethod, testObject);
                }
                finally
                {
                    foreach (MethodInfo testCleanupMethod in testCleanupMethods)
                    {
                        passed &= InvokeTestMethod(testCleanupMethod, testObject);
                    }
                }

                if (passed)
                {
                    TestRunner.LogLine("Passed.");
                }
            }
        }

        /// <summary>
        /// Runs the specified test method.
        /// </summary>
        /// <param name="method">
        /// <see cref="MethodInfo"/> object representing a method marked with 
        /// <see cref="TestMethodAttribute"/>.
        /// </param>
        /// <param name="testObject">
        /// Instance of a class marked with <see cref="TestClassAttribute"/>.
        /// </param>
        /// <returns>
        /// <code>True</code> if method completed with expected results
        /// -or-
        /// <code>False</code> if method completed with unexpected results.
        /// </returns>
        private static bool InvokeTestMethod(MethodInfo method, object testObject)
        {
            ExpectedExceptionAttribute[] expectedExceptions = (ExpectedExceptionAttribute[])
                method.GetCustomAttributes(typeof(ExpectedExceptionAttribute), false);
            try
            {
                method.Invoke(testObject, null);
                if (expectedExceptions.Length > 0)
                {
                    // Report test failure
                    CompilerError error = new CompilerError();
                    error.ErrorText = string.Format(
                        CultureInfo.CurrentCulture,
                        "{0}.{1} was expected to throw {2}.",
                        method.DeclaringType.Name,
                        method.Name,
                        expectedExceptions[0].ExceptionType.Name);
                    error.IsWarning = true; // Must report all test failures as warnings to allow log to be written to output file
                    TestRunner.errors.Add(error);

                    TestRunner.LogLine("Failed. {0} was expected.", expectedExceptions[0].ExceptionType.Name);
                    return false;
                }
            }
            catch (TargetInvocationException exception)
            {
                foreach (ExpectedExceptionAttribute expectedException in expectedExceptions)
                {
                    if (exception.InnerException.GetType() == expectedException.ExceptionType)
                    {
                        return true;
                    }
                }

                // Find stack frame of the method
                StackTrace stackTrace = new StackTrace(exception.InnerException, true);
                StackFrame stackFrame = null;
                for (int i = 0; i < stackTrace.FrameCount; i++)
                {
                    stackFrame = stackTrace.GetFrame(i);
                    if (stackFrame.GetMethod() == method)
                    {
                        break;
                    }
                }

                // Report test failure
                CompilerError error = new CompilerError();
                error.ErrorText = string.Format(
                    CultureInfo.CurrentCulture,
                    "{0} has thrown {1}: {2}",
                    stackFrame.GetMethod().Name,
                    exception.InnerException.GetType().Name,
                    exception.InnerException.Message);
                error.FileName = stackFrame.GetFileName();
                error.Line = stackFrame.GetFileLineNumber();
                error.IsWarning = true; // Must report all test failures as warnings to allow log to be written to output file
                TestRunner.errors.Add(error);

                TestRunner.Log("Failed. ");
                TestRunner.LogLine(exception.InnerException.ToString());
                return false;
            }

            return true;
        }

        /// <summary>
        /// Writes a <paramref name="message"/> to the <see cref="Trace"/> log and the transformation output.
        /// </summary>
        /// <param name="message">A format <see cref="string"/>.</param>
        /// <param name="args">An <see cref="object"/> array containing zero or more objects to format.</param>
        private static void Log(string message, params object[] args)
        {
            string formattedMessage = string.Format(CultureInfo.CurrentCulture, message, args);
            Trace.Write(formattedMessage);
            TestRunner.log.Append(formattedMessage);
        }

        /// <summary>
        /// Writes a <paramref name="message"/> to the <see cref="Trace"/> log and the transformation output.
        /// </summary>
        /// <param name="message">A format <see cref="string"/>.</param>
        /// <param name="args">An <see cref="object"/> array containing zero or more objects to format.</param>
        private static void LogLine(string message, params object[] args)
        {
            string formattedMessage = string.Format(CultureInfo.CurrentCulture, message, args);
            Trace.WriteLine(formattedMessage);
            TestRunner.log.AppendLine(formattedMessage);
        }
    }
}